////////////////////////////////////////////////////////////////////////////
//
//  Crytek Engine Source File.
//  Copyright (C), Crytek Studios, 2002.
// -------------------------------------------------------------------------
//  File name:   ParticleEnviron.cpp
//  Created:     28/08/2007 by Scott Peter
//  Description: World environment interface
// -------------------------------------------------------------------------
//  History:		 Refactored from ParticleEmitter.cpp
//
////////////////////////////////////////////////////////////////////////////

#include "StdAfx.h"
#include "ParticleEnviron.h"
#include "ParticleEmitter.h"
#include "VisAreas.h"

//////////////////////////////////////////////////////////////////////////
// SPhysEnviron implementation.
//////////////////////////////////////////////////////////////////////////

void SPhysEnviron::Clear()
{
	m_UniformForces = SExternForces(ZERO);
	m_tUnderWater = Trinary_If_False;
	m_nNonUniformFlags = 0;
	m_nNonCachedFlags = 0;
	m_NonUniformAreas.resize(0);
}

void SPhysEnviron::FreeMemory()
{
	Clear();
	stl::free_container(m_NonUniformAreas);
}

void SPhysEnviron::GetWorldPhysAreas( uint32 nFlags, bool bNonUniformAreas )
{
  FUNCTION_PROFILER_SYS(PARTICLE);

	Clear();

	// Mark areas as queried.
	m_nNonUniformFlags |= EFF_LOADED;

	Vec3 vWorldSize( GetTerrain() ? float(GetTerrain()->GetTerrainSize()) : 0.f );

	// Atomic iteration.
	for (IPhysicalEntity* pArea = 0; pArea = GetPhysicalWorld()->GetNextArea(pArea); )
	{
		// Initial check for areas of interest.
		int nAreaFlags = 0;
		Plane plWater = m_UniformForces.plWater;

		pe_params_area parea;
		if (!pArea->GetParams(&parea))
			continue;

		// Determine which forces are in area.
		if (!is_unused(parea.gravity))
			nAreaFlags |= ENV_GRAVITY;

		pe_params_buoyancy pbuoy;
		if (pArea->GetParams(&pbuoy))
		{
			if (pbuoy.iMedium == 1)
				nAreaFlags |= ENV_WIND;
			else if (pbuoy.iMedium == 0 && !is_unused(pbuoy.waterPlane.n))
			{
				plWater.SetPlane(pbuoy.waterPlane.n, pbuoy.waterPlane.origin);
				nAreaFlags |= ENV_WATER;
			}
		}

		// Skip if unneeded gravity or wind. 
		// All emitters require m_tUnderwater intersection test.
		nAreaFlags &= (nFlags | ENV_WATER);
		if (nAreaFlags == 0)
			continue;

		// Query area for intersection and forces, uniform only.
		pe_status_area sarea;
		sarea.ctr.zero();
		sarea.size = vWorldSize;
		sarea.bUniformOnly = true;
		int nStatus = pArea->GetStatus(&sarea);
		if (!nStatus)
			continue;

		if (nAreaFlags & ENV_WATER)
		{
			m_tUnderWater = Trinary_Both;

			// Skip if unneeded water particle collision.
			if (!(nAreaFlags & nFlags))
				continue;
		}

		pe_status_pos spos;
		Matrix33 matArea;
		spos.pMtx3x3 = &matArea;
		if (!pArea->GetStatus(&spos))
			continue;

		// Get correct force directions.
		if (parea.bUniform == 2)
		{
			if (nAreaFlags & ENV_GRAVITY)
				parea.gravity = spos.q * parea.gravity;
			if (nAreaFlags & ENV_WIND)
				pbuoy.waterFlow = spos.q * pbuoy.waterFlow;
		}
 		
		SExternForces area_forces;
		area_forces.vAccel = nAreaFlags & ENV_GRAVITY ? parea.gravity : Vec3(0.f);
		area_forces.vWind = nAreaFlags & ENV_WIND ? pbuoy.waterFlow : Vec3(0.f);
		area_forces.plWater = plWater;

		if (nStatus < 0)
		{
			if (bNonUniformAreas)
			{
				SArea& area = *(new SArea);
				m_NonUniformAreas.push_back(&area);

				area.m_pArea = pArea;
				area.m_nFlags = nAreaFlags;
				area.m_pLock = sarea.pLockUpdate;
				area.m_bbArea.min = spos.BBox[0] + spos.pos;
				area.m_bbArea.max = spos.BBox[1] + spos.pos;
				area.m_Forces = area_forces;

				if (nAreaFlags & ENV_WATER)
				{
					// Expand water test area 
					area.m_bbArea.min.z = -fHUGE;
				}

				pe_params_foreign_data pfd;
				if (pArea->GetParams(&pfd))
				{
					if (pfd.iForeignFlags & PFF_OUTDOOR_AREA)
						area.m_bOutdoorOnly = true;
				}

				area.m_bCacheForce = false;

				if (nAreaFlags & (ENV_GRAVITY | ENV_WIND))
				{
					// Test whether we can cache force information for quick evaluation.
					if (parea.size.GetVolume() > 0.f && parea.pGeom && parea.pGeom->GetType() == GEOM_BOX)
					{
						area.m_bRadial = !parea.bUniform;
						area.m_fInnerRadius = parea.falloff0;

						// Construct world-to-unit-sphere transform.
						area.m_vCenter = spos.pos;
						area.m_matToLocal = (matArea * Matrix33::CreateScale(parea.size)).GetInverted();
						area.m_bCacheForce = true;
					}
				}
				if (!area.m_bCacheForce)
					m_nNonCachedFlags |= nAreaFlags;
			}
			m_nNonUniformFlags |= nAreaFlags;
		}
		else
		{
			// Uniform area.
			m_UniformForces.Add( area_forces, nAreaFlags );
		}
	}
}

void SPhysEnviron::GetPhysAreas( SPhysEnviron const& envSource, AABB const& box, uint32 nFlags, const CParticleEmitter* pEmitterSkip, bool bIndoors, bool bNonUniformAreas, const Vec3 &vPosition )
{
  FUNCTION_PROFILER_SYS(PARTICLE);

	Clear();

	// Mark areas as queried.
	m_nNonUniformFlags |= EFF_LOADED;

	m_UniformForces.vAccel = envSource.m_UniformForces.vAccel;
	if (!bIndoors)
	{
		m_UniformForces.vWind = envSource.m_UniformForces.vWind;
		m_UniformForces.plWater = envSource.m_UniformForces.plWater;

		// Set status of box with respect to water plane.
		float fDistMin, fDistMax;
		Distance::AABB_Plane( &fDistMin, &fDistMax, box, m_UniformForces.plWater );
		m_tUnderWater = fDistMin > 0.f ? Trinary_If_False
			: fDistMax < 0.f ? Trinary_If_True
			: Trinary_Both;
	}

	if (bNonUniformAreas && (envSource.m_nNonUniformFlags & (nFlags | ENV_WATER)) && !box.IsReset())
	{
		for_array (i, envSource.m_NonUniformAreas)
		{
			SArea& area = envSource.m_NonUniformAreas[i];
			if (area.m_nFlags & (nFlags | ENV_WATER))
			{
				// Query area for intersection and forces, uniform only.
				if (!bIndoors || !area.m_bOutdoorOnly)
				{
					// Test bb intersection.
					if (area.m_bbArea.IsIntersectBox(box))
					{
						if (pEmitterSkip)
						{
							pe_params_foreign_data fd;
							if (area.m_pArea->GetParams(&fd))
							{
								if (fd.pForeignData == pEmitterSkip)
									continue;
							}
						}

						pe_status_area sarea;
						sarea.ctr = box.GetCenter();
						sarea.size = box.GetSize() * 0.5f;
						sarea.bUniformOnly = true;
						int nStatus = area.m_pArea->GetStatus(&sarea);

						if (nStatus)
						{
							// Update underwater status.
							if ((area.m_nFlags & ENV_WATER) && m_tUnderWater != Trinary_If_True)
							{
								// Check box underwater status.
								float fDistMin, fDistMax;
								Distance::AABB_Plane( &fDistMin, &fDistMax, box, area.m_Forces.plWater );
								if (fDistMax < 0.f)
								{
									// Underwater.
									m_tUnderWater = nStatus < 0? Trinary_Both : Trinary_If_True;
								}
								else if (fDistMin < 0.f)
								{
									// Partially underwater.
									m_tUnderWater = Trinary_Both;
								}
							}

							if (area.m_nFlags & nFlags)
							{
								if (nStatus < 0)
								{
									// Store in local nonuniform area list.
									m_NonUniformAreas.push_back(&area);
									m_nNonUniformFlags |= area.m_nFlags;
									if (!area.m_bCacheForce)
										m_nNonCachedFlags |= area.m_nFlags;
								}
								else
								{
									// Uniform within this volume.
									m_UniformForces.Add( area.m_Forces, area.m_nFlags );
								}
							}
						}
					}
				}
			}
		}
	}
}

void SPhysEnviron::CheckNonUniformAreas( Vec3 const& vPos, Vec3& vGravity, Vec3& vWind, uint32 nFlags ) const
{
	for_array (i, m_NonUniformAreas)
	{
		if (m_NonUniformAreas[i].m_nFlags & nFlags)
			m_NonUniformAreas[i].GetForce( vPos, vGravity, vWind );
	}
}

SPU_NO_INLINE bool SPhysEnviron::PhysicsCollision( ray_hit& hit, Vec3 const& vStart, Vec3 const& vEnd, float fRadius, uint32 nEnvFlags, IPhysicalEntity* pTestEntity )
{
	FUNCTION_PROFILER_SYS(PARTICLE);

	bool bHit = false;
	Vec3 vMove = vEnd-vStart;
	float fMoveSqr = vMove.GetLengthSquared();
	if (fMoveSqr == 0.f)
		return false;
	float fInvMove = isqrt_tpl(fMoveSqr);

	// Extend movement by particle radius.
	float fRadiusRel = fRadius * fInvMove;
	vMove *= 1.f + fRadiusRel;

	float fMoveNorm = 0.f;
	ZeroStruct(hit);
	hit.dist = 1.f;

	// Collide terrain first (if set as separately colliding).
	if ((nEnvFlags & ~ENV_COLLIDE_PHYSICS & ENV_TERRAIN) && !pTestEntity && GetTerrain())
	{
		nEnvFlags &= ~ENV_TERRAIN;
		CHeightMap::SRayTrace rt;
		if (GetTerrain()->RayTrace(vStart, vStart+vMove, &rt, GetDefSID()))
		{
			if ((fMoveNorm = rt.vNorm * vMove) < 0.f)
			{
				bHit = true;
				hit.dist = rt.fInterp;
				hit.pt = rt.vHit;
				hit.n = rt.vNorm;
				if (rt.pMaterial)
					hit.surface_idx = rt.pMaterial->GetSurfaceTypeId();
				else
					hit.surface_idx = 0;
				hit.bTerrain = true;
			}
		}
	}

	if (pTestEntity)
	{
		// Test specified entity only.
		bHit = GetPhysicalWorld()->RayTraceEntity( pTestEntity, vStart, vMove, &hit )
			&& hit.n * vMove < 0.f;
	}
	else if (hit.dist > 0.f)
	{
		int ent_collide = 0;
		if (nEnvFlags & ENV_TERRAIN)
			ent_collide |= ent_terrain;
		if (nEnvFlags & ENV_STATIC_ENT)
			ent_collide |= ent_static;
		if (nEnvFlags & ENV_DYNAMIC_ENT)
			ent_collide |= ent_rigid | ent_sleeping_rigid | ent_living | ent_independent;

		IF (ent_collide, false)
		{
			// rwi_ flags copied from similar code in CParticleEntity.
			ray_hit hit_loc;
			if (GetPhysicalWorld()->RayWorldIntersection(
				vStart, vMove*hit.dist,
				ent_collide | ent_no_ondemand_activation,
				sf_max_pierceable|(geom_colltype_ray|geom_colltype13)<<rwi_colltype_bit | rwi_colltype_any | rwi_ignore_noncolliding,
				&hit_loc, 1) > 0
			)
			{
				if ((fMoveNorm = hit_loc.n * vMove) < 0.f)
				{
					bHit = true;
					hit = hit_loc;
					hit.dist *= fInvMove;
				}
			}
		}
	}

	if (bHit)
	{
		hit.pt += hit.n * fRadius;
		hit.dist -= div_min(fRadius, -fMoveNorm, 1.f);
		hit.dist = clamp(hit.dist, 0.f, 1.f);
	}
	return bHit;
}

void SPhysEnviron::SArea::GetForcePhys( Vec3 const& vPos, Vec3& vGravity, Vec3& vWind ) const
{
  FUNCTION_PROFILER_SYS(PARTICLE);

	pe_status_area sarea;
	sarea.ctr = vPos;
	if (m_pArea->GetStatus(&sarea))
	{
		if (!is_unused(sarea.gravity))
			vGravity = sarea.gravity;
		if (sarea.pb.iMedium == 1 && !is_unused(sarea.pb.waterFlow))
			vWind += sarea.pb.waterFlow;
	}
}

void SPhysEnviron::SArea::GetForce( Vec3 const& vPos, Vec3& vGravity, Vec3& vWind ) const
{
	if (!m_bCacheForce)
	{
#if !defined(__SPU__)
		GetForcePhys( vPos, vGravity, vWind );


#endif
	}
	else
	{
	  FUNCTION_PROFILER_SYS(PARTICLE);

		Vec3 vPosRel = vPos - m_vCenter;
		float fDistSq = (m_matToLocal * vPosRel).GetLengthSquared();
		if (fDistSq < 1.f)
		{
			float fStrength = div_min(1.f - sqrt_fast_tpl(fDistSq), 1.f - m_fInnerRadius, 1.f);

			if (m_bRadial)
			{
				Vec3 vForce = vPosRel.GetNormalized();
				if (m_nFlags & ENV_GRAVITY)
					vGravity = vForce * (m_Forces.vAccel.z * fStrength);
				if (m_nFlags & ENV_WIND)
					vWind += vForce * (m_Forces.vWind.z * fStrength);
			}
			else
			{
				if (m_nFlags & ENV_GRAVITY)
					vGravity = m_Forces.vAccel * fStrength;
				if (m_nFlags & ENV_WIND)
					vWind += m_Forces.vWind * fStrength;
			}
		}

#ifdef _DEBUG
		Vec3 vPhysGrav = vGravity, vPhysWind = vWind;
		GetForcePhys( vPos, vPhysGrav, vPhysWind );
		assert(vPhysGrav.IsEquivalent(vGravity));
		assert(vPhysWind.IsEquivalent(vWind));
#endif
	}
}

SPU_NO_INLINE float SPhysEnviron::NonUniformDistanceFromWater( Plane& plWater, Vec3 const& vPos, float fSense, float fMaxDist ) const
{
	// Check for non-uniform areas.
	plWater = m_UniformForces.plWater;
	float fDistMin = m_UniformForces.plWater.DistFromPlane(vPos);
	IF ((m_nNonUniformFlags & ENV_WATER), false)
	{
#if !defined(__SPU__)
	  FUNCTION_PROFILER_SYS(PARTICLE);

		CTerrain* pTerrain = GetTerrain();
		float fTrrZ = -fHUGE;
	
		for_array (i, m_NonUniformAreas)
		{
			const SArea& area = m_NonUniformAreas[i];
			if (area.m_nFlags & ENV_WATER)
			{
				float fDist = area.m_Forces.plWater.DistFromPlane(vPos) * fSense;
				if (fDist < fMaxDist)
				{
					if (GetCVars()->e_ParticlesDebug & AlphaBit('t'))
					{
						if (pTerrain)
						{
							// Optimisation: Before doing expensive check on complex area,
							// test if either particle or water level is under terrain.
							// If so, ignore all (non-global) areas.
							fTrrZ = pTerrain->GetZSafe((int)vPos.x, (int)vPos.y, GetDefSID()) - vPos.z;
							pTerrain = NULL;
						}
						if (fTrrZ >= min(fMaxDist, -fDist))
							break;
					}
					pe_status_contains_point st;
					st.pt = vPos - area.m_Forces.plWater.n * fDist;
					if (area.m_pArea->GetStatus(&st))
					{
						plWater = area.m_Forces.plWater;
						if (fDist <= 0.f)
							return fDist;
						fMaxDist = fDist;
					}
				}
			}
		}


#endif
	}

	return fMaxDist;
}

/*
	Emitters render either entirely inside or entirely outside VisAreas (due to rendering architecture).
		Emitter origin in vis area:
			Params.VisibleInside == If_False? Don't render
			else entire emitter in vis area: we're good
			else (intersecting): Test each particle for dist, shrink / fade-out near boundary
		Emitter origin outside vis areas:
			Params.VisibleInside == If_True ? Don't render
			else entirely outside vis areas: we're good
			else (intersecting): Test each particle for dist, shrink / fade-out near boundary
*/

void SVisEnviron::Update( Vec3 const& vOrigin, AABB const& bb )
{
	if (!m_bValid)
	{
		// Determine emitter's vis area, by origin.
		FUNCTION_PROFILER_SYS(PARTICLE);

		m_pVisArea = Get3DEngine()->GetVisAreaFromPos(vOrigin);
		m_pVisNodeCache = 0;

		// See whether it crosses boundaries.
		if (m_pVisArea)
			m_bCrossesVisArea = m_pVisArea->GetAABBox()->IsIntersectBox(bb);
		else
			m_bCrossesVisArea = Get3DEngine()->IntersectsVisAreas(bb, &m_pVisNodeCache);

		m_bValid = true;
	}

	m_pClipVisArea = 0;
	if (m_bCrossesVisArea)
	{
		// Clip only against cam area.
		IVisArea* pVisAreaCam = GetVisAreaManager()->GetCurVisArea();
		if (pVisAreaCam != m_pVisArea)
		{
			if (pVisAreaCam)
			{
				if (pVisAreaCam->GetAABBox()->IsIntersectBox(bb))
				{
					if (!m_pVisArea || !pVisAreaCam->FindVisArea(m_pVisArea, 3, true))
						m_pClipVisArea = pVisAreaCam;
				}
			}
			else if (m_pVisArea)
				m_pClipVisArea = m_pVisArea;
		}
	}
}

//////////////////////////////////////////////////////////////////////////
// GeomRef functions.

#include "ICryAnimation.h"
#include "GeomQuery.h"

void NormalizeEmitGeom( GeomRef& geom, EGeomType eAttachType )
{
	// Remove references to unneeded objects.
	if (eAttachType == GeomType_Physics)
	{
		if (geom.m_pChar)
		{
			// Substitute articulated physics if found.
			if (ISkeletonPose* pPose = geom.m_pChar->GetISkeletonPose())
			{
				IPhysicalEntity* pPhys = pPose->GetCharacterPhysics();
				if (!pPhys)
					pPhys =	pPose->GetCharacterPhysics(0);
				if (pPhys)
					geom.m_pPhysEnt = pPhys;
			}
		}
	}
	else
	{
		geom.m_pPhysEnt = 0;
	}
	if (eAttachType != GeomType_Render)
	{
		geom.m_pStatObj = 0;
		geom.m_pChar = 0;
	}
}

void GetAABB( AABB& bb, QuatTS const& tLoc, GeomRef const& geom )
{
	bb.Reset();
	if (geom.m_pStatObj)
		bb.SetTransformedAABB(Matrix34(tLoc), geom.m_pStatObj->GetAABB());
	else if (geom.m_pChar)
		bb.SetTransformedAABB(Matrix34(tLoc), geom.m_pChar->GetAABB());
	else if (geom.m_pPhysEnt)
	{
		pe_status_pos pos;
		if (geom.m_pPhysEnt->GetStatus(&pos))
			// Box is already axis-aligned, but not offset.
			bb = AABB(pos.pos + pos.BBox[0], pos.pos + pos.BBox[1]);
	}
}

float GetExtent( GeomRef const& geom, EGeomForm eForm )
{
  FUNCTION_PROFILER_SYS(PARTICLE);

	if (geom.m_pStatObj)
		return geom.m_pStatObj->GetExtent(eForm);
	else if (geom.m_pChar)
		return geom.m_pChar->GetExtent(eForm);
	else if (geom.m_pPhysEnt)
	{
		// status_extent query caches extent in geo.
		pe_status_extent se;
		se.eForm = eForm;
		geom.m_pPhysEnt->GetStatus(&se);
		return se.extent;
	}
	return 0.f;
}

void GetRandomPos( PosNorm& ran, GeomRef const& geom, EGeomForm eForm, QuatTS const& tWorld )
{
#ifndef __SPU__
  FUNCTION_PROFILER_SYS(PARTICLE);

	if (geom.m_pStatObj)
	{
		geom.m_pStatObj->GetRandomPos(ran, eForm);
		Transform(ran, tWorld);
	}
	else if (geom.m_pChar)
	{
		geom.m_pChar->GetRandomPos(ran, eForm);
		Transform(ran, tWorld);
	}
	else if (geom.m_pPhysEnt)
	{
		pe_status_random sr;
		sr.eForm = eForm;
		geom.m_pPhysEnt->GetStatus(&sr);
		ran = sr.ran;
	}

#ifdef _DEBUG
	AABB bb;
	GetAABB(bb, tWorld, geom);
	assert(bb.IsContainPoint(ran.vPos));
#endif
#endif
}
